23.08.11
오늘 한 일
- 카공실록 리뷰 등록 api 연결 완료
- gloddy 디자인 시스템 적용
😀
프로그라피 랜딩 페이지 작업을 맡기로 했다.
디자이너 운영진 누나가 먼저 작업 제안을 해서 나도 하기로 했다.
아직 뭔가 이루어진게 없어서 확정은 아니지만, 만약 한다면 실제로 서비스에 적용되는 작업이라 더욱 더 재밌을 것 같다. 😀
Button 공통 컴포넌트 만들기
twMerge 사용 시 커스텀 클래스끼리 병합하는 문제
예를 들어 text-white
와 text-h1
을 병합하면 text-white text-h1
이 되어야 하는데, 마지막에 병합된 클래스가 적용되는 문제가 있었다.
다행히 관련 이슈를 찾아서 해결할 수 있었다.
const customTwMerge = extendTailwindMerge({
classGroups: {
'font-size': [
{
text: Object.keys(fontSizes),
},
],
},
});
const cn = (...inputs: ClassValue[]) => {
return customTwMerge(clsx(inputs));
};
이제 clsx와 twMerge를 사용할 때 cn
유틸 함수를 사용해도 된다!
Button 컴포넌트
미리 컬러를 정의해두었기 때문에, 수월하게 진행할 수 있었다.
Button 코드
import cn from '@/utils/cn';
export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
/**
* 버튼의 크기를 설정합니다. small: 48px, medium: 56px (default: medium)
*/
size?: 'small' | 'medium';
/**
* 버튼의 색상을 설정합니다. (default: solid-primary)
*/
variant?:
| 'solid-primary'
| 'solid-default'
| 'solid-secondary'
| 'outline-warning'
| 'solid-warning';
children: React.ReactNode;
}
export default function Button({
size = 'medium',
variant = 'solid-primary',
className,
disabled,
children,
...props
}: ButtonProps) {
return (
<button
className={cn(
'flex items-center justify-center rounded-8 px-24 py-16 text-subtitle-2',
{
'h-56': size === 'medium',
'h-48': size === 'small',
'bg-primary text-sign-white disabled:bg-primary-light': variant === 'solid-primary',
'bg-button text-sign-secondary disabled:bg-sub disabled:text-sign-caption':
variant === 'solid-default',
'bg-brand-color text-sign-brand disabled:text-sign-white': variant === 'solid-secondary',
'border border-warning bg-warning-color text-warning disabled:border-warning-light disabled:bg-white disabled:text-warning-light':
variant === 'outline-warning',
'bg-warning text-sign-white disabled:bg-sub disabled:text-sign-caption':
variant === 'solid-warning',
},
className
)}
disabled={disabled}
{...props}
>
{children}
</button>
);
}
ButtonGroup 컴포넌트
ButtonGroup 컴포넌트는 Button 컴포넌트를 감싸는 역할을 한다.
position
이 bottom
인 경우, 화면 하단에 고정되어야 한다.
그리고 Group 안 버튼이 하나인 경우와 두 개인 경우를 구분해야 했다.
- 하나인 경우: 버튼이 화면 너비의 100%를 차지해야 한다.
- 두 개인 경우: 왼쪽 버튼은 회색 버튼, 오른쪽 버튼은 파란색 버튼이고 두 버튼 사이에는 8px의 간격이 있어야 한다. 그리고 파란색 버튼의 flex-grow는 1이다.
🤔사용하는 곳에서 버튼 스타일을 따로 주고 싶지 않은데 어떻게 해야 할까?
나는 이렇게 쓰고 싶었다.
<ButtonGroup>
<Button>건너뛰기</Button>
<Button>확인</Button>
</ButtonGroup>
이렇게 하기 위해서는 children으로 가져온 Button 컴포넌트에 props를 전달해야 한다.
그래서 먼저 가져온 children이 Button 컴포넌트인지 확인하고, Button 컴포넌트라면 props를 전달하도록 했다.
1. 가져온 children이 Button 컴포넌트인지 확인하기
isValidElement
를 사용해서 리액트 엘리먼트인지 확인한다.
그다음 name
을 사용해서 Button 컴포넌트인지 확인한다.
const validChildren = Children.toArray(children).filter(
child =>
isValidElement(child) &&
(
child.type as {
name: string;
}
).name === 'Button'
) as ReactElement[];
2. Button 컴포넌트라면 props를 전달하기
cloneElement
를 사용해서 props를 전달해주어 복사한 엘리먼트를 리턴한다. 이 때, 기존에 있던 className
이 유지되도록 cn
유틸 함수를 사용한다.
const renderElements = (elements: ReactElement[]) => {
if (elements.length === 1) {
const props = elements[0].props as ButtonProps;
return cloneElement(elements[0], {
className: cn('w-full', props.className),
});
}
return (
<div className="flex gap-8">
{elements.map((element, index) => {
const props = elements[index].props as ButtonProps;
if (index === 0) {
return cloneElement(element, {
className: cn('flex-shrink-0', props.className),
variant: props.variant ?? 'solid-default',
});
}
return cloneElement(element, { className: cn('w-full', props.className) });
})}
</div>
);
};
최종 코드
이제 ButtonGroup 컴포넌트를 사용할 수 있다..!
하나인 경우
<ButtonGroup>
<Button>확인</Button>
</ButtonGroup>
두 개인 경우
<ButtonGroup>
<Button>건너뛰기</Button>
<Button>확인</Button>
</ButtonGroup>
ButtonGroup 코드
import cn from '@/utils/cn';
import { Children, type ReactElement, cloneElement, isValidElement } from 'react';
import type { ButtonProps } from './Button';
interface ButtonGroupProps {
/**
* 버튼 그룹의 위치를 설정합니다. (default: bottom)
*/
position?: 'bottom' | 'contents';
children: React.ReactNode;
}
export default function ButtonGroup({ position = 'bottom', children }: ButtonGroupProps) {
const validChildren = Children.toArray(children).filter(
child =>
isValidElement(child) &&
(
child.type as {
name: string;
}
).name === 'Button'
) as ReactElement[];
const renderElements = (elements: ReactElement[]) => {
if (elements.length === 1) {
const props = elements[0].props as ButtonProps;
return cloneElement(elements[0], {
className: cn('w-full', props.className),
});
}
return (
<div className="flex gap-8">
{elements.map((element, index) => {
const props = elements[index].props as ButtonProps;
if (index === 0) {
return cloneElement(element, {
className: cn('flex-shrink-0', props.className),
variant: props.variant ?? 'solid-default',
});
}
return cloneElement(element, { className: cn('w-full', props.className) });
})}
</div>
);
};
return (
<div
className={cn('mx-auto border-t-1 border-divider bg-white p-20 pt-7', {
'fixed inset-x-0 bottom-0 max-w-450': position === 'bottom',
})}
>
{renderElements(validChildren)}
</div>
);
}
내일 할 일
- 프로그라피 팀 회의
- 카공실록 개발
- gloddy 개발